Skip to content

Add Axum production startup and bounded shutdown - #406

Open
ChristianPavilonis wants to merge 7 commits into
docs/outbound-http-specfrom
issue-392-396-production-hosting-spec
Open

ChristianPavilonis wants to merge 7 commits into
docs/outbound-http-specfrom
issue-392-396-production-hosting-spec

Conversation

@ChristianPavilonis

@ChristianPavilonis ChristianPavilonis commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Current review readiness — 2026-11-10

Ready for review at the publication head below. All29 current hosted checks passed. Reviewers: aram356, prk-Jr, dhruv8sh.

Review transitions are independent of TS work and the combined integration audit; #462 remains draft. This current status supersedes historical draft-only prerequisites below without claiming unexecuted deployment/manual acceptance complete. All three reviewers are requested. Stack465 is the current grouping; #453 is excluded.

Current review-stack checkpoint

Publication head: 9f090e573206cf0d8c21c1ce0067afffd69f12db. This cleanup preserves the existing feature unit and PR identity. The additional change is the completed generated-native build correction: image-only code now follows the scaffold's existing Fastly-example selection, with no request-policy change. Its focused generator/native proof is retained; fresh per-branch hosted checks are not assumed green.

Review order: #275 → #406 → #407 → #409 → #414 → #422 → #439 → #454. Draft integration checkpoint: #462. New SDK/group/codec/phase development and unrelated TS work are not prerequisites or additions to this PR. Earlier notes below retain their original tested revisions and limits; they are not a whole-tree pass for this new head.

Earlier implementation and proof notes

RC refresh checkpoint, 2026-11-10

Current head: 1d8e42e1cba52610e6655965e9941f42680b53ab.

Current #275 hosting port, with bounded startup/config/ingress and existing callback/egress ownership retained. The invalid-present native-limit and Windows prefix-identity capture bugs found by review are fixed. Windows semantics were simulated at the private parser boundary, not run on Windows.

Fresh CI exposed missing fixture validator metadata, an always-compiled test listener helper and a trigger assertion still expecting main-only PRs. The fixture lock is synced, the helper now has the same test/test-utils gate as its caller, and the exact workflow test now requires all PR bases while retaining main-only push. No lint suppression or production/deployment trigger expansion. Parent proof includes 301 Axum unit tests, axum-only strict Clippy and 35 Node CI tests.

Draft pending fresh hosted checks and the combined RC audit. Two unchanged native default-parallel deadline misses remain recorded and unclassified; serial passes and untouched-foundation parallel passes do not clear them. No timing bound was relaxed. No deployment, main merge, security-alert dismissal or review-thread resolution occurred. Historical design/review context follows.


Summary

  • Add explicit production startup so required settings, stores, transport, and app-owned initialization succeed before accepting traffic. Keep permissive development entrypoints.
  • Extend the existing native HTTP/1 runner with SIGINT/Unix SIGTERM handling, tracked graceful draining, a finite shutdown budget, and forced non-success exit.
  • Add portable app-owned probes, shared typed startup config loading, retained handler state, and subprocess acceptance tests.

This PR is stacked on #275, with base branch docs/outbound-http-spec. The implementation started at 5527b58. It remains draft because #275 has advanced to 91f4af38, five parent-only commits beyond the common base 5527b58. Parent integration and re-audit, including the previously identified dev_server.rs conflict, remain outstanding. The recorded checks are for this branch, not integration with the current prerequisite. This relationship audit performed no merge or rebase. Integrate the current contracts and repeat affected validation before marking ready.

Changes

Crate / File Change
edgezero-adapter-axum/src/dev_server.rs, run_options.rs Explicit production and embedding APIs, prepared registries, app initializer, lifecycle ownership, signals, and bounded drain
Axum connection/service modules Private dispatch gate, live lifecycle reader, and graceful HTTP/1 connection shutdown
Axum config/KV stores Strict required config snapshots, explicit roots, same-name KV aliases, and redacted startup failures without delete-and-restart advice
edgezero-core/src/app.rs, router.rs Startup-only typed state insertion with copy-on-write isolation and old-admission invalidation
edgezero-core/src/extractor.rs, probe.rs Context-free typed config loading shared with request extraction, plus ordinary readiness/liveness actions
Native and portable test fixtures Real process signals, finite/forced drain, startup failure/redaction, embedding, restart persistence, aliasing, stopped-writer backup/restore, and projected read-only config
Production hosting design Contracts, acceptance boundaries, executed checks, and remaining integration/deployment work

Closes

Closes #392.
Closes #396.

Part of #391. This PR remains draft until the pending integration and acceptance work below is complete.

Remaining acceptance includes production adoption/storage guides, standalone release and Docker UID evidence, unreadable/unusable mount cases, retained-handle lock evidence, and the accepted-#275 re-audit.

Test plan

  • cargo test --workspace --all-targets
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo fmt --all -- --check
  • cargo check --workspace --all-targets --features "fastly cloudflare spin"
  • Full WASM adapter builds for Fastly, Spin, and Cloudflare
  • examples/app-demo workspace: cd examples/app-demo && cargo test --workspace --all-targets
  • Docs build: cd docs && npm run lint && npm run format && npm run build
  • Manual testing via edgezero serve --adapter axum
  • cargo test -p edgezero-core --all-targets
  • cargo test -p edgezero-adapter-axum --all-targets --all-features, including all 13 production-hosting subprocess tests
  • cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin
  • cargo check -p edgezero-core --test production_handlers --target <target> for wasm32-wasip1, wasm32-wasip2, and wasm32-unknown-unknown
  • git diff --check

Checks passed on this branch, not on a merge with the newer prerequisite. Native subprocess tests use debug fixtures and the current user's UID, not a release container image. Shutdown cancellation is cooperative; blocking application code still requires a supervisor hard-kill boundary.

Checklist

  • Changes follow CLAUDE.md conventions
  • No Tokio deps added to core or adapter crates
  • Route params use {id} syntax, not :id
  • Types imported from edgezero_core, not http
  • Store wiring goes through KvRegistry / ConfigRegistry / SecretRegistry
  • New code has tests
  • No secrets or credentials committed

@ChristianPavilonis
ChristianPavilonis force-pushed the issue-392-396-production-hosting-spec branch from 3ab2209 to 49ba021 Compare November 10, 2026 19:40
@ChristianPavilonis
ChristianPavilonis marked this pull request as ready for review November 10, 2026 22:57

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review

Summary

Adds explicit production startup to the Axum runner: an app-owned initializer, startup-only typed state, portable probe handlers, and draining on SIGINT/SIGTERM within a fixed time budget. Requesting changes for three reasons:

  • The subprocess suite that tests this behavior never runs in CI, and it fails intermittently in parallel.
  • Two existing paths regress: embedding installs a global logger, and dev startup no longer shows its URL or the real cause of a bind failure.
  • The user docs no longer match the dev behavior.

Findings

Blocking

  • 🔧 Subprocess suite never runs in CI (production_hosting.rs:2)

  • 🔧 Port reservation race; 4 of 9 parallel runs failed locally (production_hosting.rs:60)

  • 🔧 Embedding installs a global logger; standalone installs it after App::build (dev_server.rs:1014)

  • 🔧 Dev startup lost the listening URL and mislabels bind failures (dev_server.rs:473, :654)

  • 🔧 Axum guide no longer matches dev behavior: docs/guide/adapters/axum.md:144-146 says shutdown "closes the owned connections rather than waiting indefinitely for a graceful drain". run_app and AxumDevServer::run now:

    • drain for up to EDGEZERO__ADAPTER__SHUTDOWN_GRACE_SECONDS (default 10);
    • handle SIGTERM, and a second signal forces exit;
    • fail dev startup on an invalid grace value.

    The variable table at axum.md:105 doesn't list the setting. None of the new public API has a guide entry: run_production_app*, run_app_with_options*, AxumRunOptions, PreparedStores, probe::{readiness, liveness}, App::insert_state, AppConfig::from_binding. At minimum, update the shutdown paragraph and the variable table in this PR.

  • ❓ Should this PR close #392 and #396? The body says Closes #392 / Closes #396, but it also lists remaining acceptance work: production adoption/storage guides, standalone release and Docker UID evidence, unreadable/unusable mounts, and retained-handle lock evidence. The design doc (line 427) says this work "does not complete all acceptance criteria for #392 or #396". Once #275 merges and this PR is retargeted to main, merging it will auto-close both issues. Should these be Part of #392 / Part of #396 until the remaining items land?

Non-blocking

  • 🤔 Existing AxumDevServer paths now take over SIGTERM; run() grace is hard-coded (dev_server.rs:507)
  • ♻️ Drain vs cancel is inferred from how the listener was obtained (dev_server.rs:610)
  • ♻️ Config failure categories depend on message strings (dev_server.rs:1080)
  • 🤔 Every KV open failure reads the same (key_value_store.rs:166)
  • ♻️ Duplicate bare-handle normalization (dev_server.rs:361)
  • ♻️ A LifecycleReader is allocated per request (service.rs:349)
  • 🤔 Test gaps: a post-exit connect check that can't fail, no subprocess coverage of the dev entrypoint, no test for dev KV alias reuse, logger path untested (production_hosting.rs:243)
  • ⛏ Local path and stale status in the design doc (2026-10-05-axum-production-startup-lifecycle-design.md:10)
  • 🌱 Mark LifecyclePhase #[non_exhaustive] (probe.rs:19)
  • ⛏ Config-store limit validation depends on whether a config store is declared (dev_server.rs:1068)

📌 Out of Scope

  • Two changes are unrelated to production hosting: widening the pull_request triggers in test.yml, format.yml and codeql.yml to every base branch (CodeQL now runs on every stacked PR), and the generator image-helper fix (9f090e57). Moving them to their own PRs, or into #275, lets each be reviewed and reverted separately.

CI Status (local, head 9f090e57)

  • fmt: PASS
  • clippy (--workspace --all-targets --all-features -D warnings): PASS
  • tests (--workspace --all-targets): PASS, but production_hosting.rs ran 0 tests
  • feature check (fastly cloudflare spin): PASS
  • spin wasm32-wasip2: PASS
  • hosting suite (-p edgezero-adapter-axum --all-features --test production_hosting, not run in CI): 3/3 serial runs PASS; 5/9 parallel runs PASS, 4 FAIL with startup bind: listener: address unavailable

@@ -0,0 +1,656 @@
//! Real local processes, public entrypoints and ordinary main termination output.
#![cfg(all(feature = "axum", feature = "test-utils", unix))]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 This suite never runs in CI

The file is gated on feature = "test-utils", and no workflow step enables axum,test-utils for cargo test. The workspace job compiles it and reports Running tests/production_hosting.rs … 0 passed. The green hosted checks therefore say nothing about signals, draining, startup failures or storage. The WASM compile check for edgezero-core/tests/production_handlers.rs has also only been run locally.

Fix: add a step to the workspace suite in test.yml (in a job with the wasm targets installed), wrapped in scripts/run_test_nonzero.sh like the other targeted steps so a silent 0-test run fails:

- name: Production hosting subprocess acceptance
  if: matrix.suite == 'workspace'
  run: |
    cargo test --locked -p edgezero-adapter-axum --features axum,test-utils --test production_hosting
    for target in wasm32-wasip1 wasm32-wasip2 wasm32-unknown-unknown; do
      cargo check --locked -p edgezero-core --test production_handlers --target "$target"
    done

Fix the port race (next comment) first, or this step will fail intermittently.

.expect("envelope")
}

fn address() -> SocketAddr {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 Port reservation race makes the suite flaky in parallel

address() binds 127.0.0.1:0, records the port and drops the listener. Before either child binds, the kernel can give the same port to another test's address() call. Locally (macOS) the suite failed 4 of 9 default-parallel runs, each time in a different test (second_signal_forces_before_budget_exhaustion, process_budget_forces_pending_handler_stream_and_zero_read_peer, both_signals_stop_idle_processes, embedding_does_not_install_process_signal_handlers). Every failure was early process failure: Error: startup bind: listener: address unavailable. All 3 --test-threads=1 runs passed. This may be the same thing as the "unclassified native default-parallel deadline misses" in the PR body.

Fix: never hand out the same port twice within the test binary. The restart test's deliberate reuse of its own addr still works:

fn address() -> SocketAddr {
    static ISSUED: Mutex<BTreeSet<u16>> = Mutex::new(BTreeSet::new());
    loop {
        let addr = TcpListener::bind("127.0.0.1:0")
            .expect("port reservation")
            .local_addr()
            .expect("address");
        if ISSUED.lock().expect("issued ports").insert(addr.port()) {
            return addr;
        }
    }
}

Collisions with other processes are still possible but rare. Serializing the suite is the alternative.

options.validate_stores(metadata)?;
let mut app = App::build::<A>(crate::AXUM_PLATFORM)
.map_err(|_error| anyhow::anyhow!("application configuration failed"))?;
init_logging::<A>(options.logging_level())?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 Logger installation: embedding installs a global logger, standalone installs it too late

  1. Embedding. run_app_with_options* install a process-wide SimpleLogger unless the app owns logging. A throwaway test that called run_app_with_options twice in one process (default Hooks, stop after 100 ms) got Ok(()) and then Err(startup logging: application: initialization failed). A host that has already installed a logger fails the same way on the first call. SimpleLogger::init also calls log::set_max_level before set_boxed_logger fails, so the failed attempt still overwrites the host's level. The embedding variants already avoid process-wide signal handlers, and the logger is process-wide state in the same way.
  2. Standalone. The logger is installed after App::build, so anything Hooks::configure logs is dropped, and its failure is reduced to a bare application configuration failed, which doesn't use the failure() format. The dev path installs the logger before app configuration.

The fixture's owns_logging() -> true means neither path is tested.

Fix: install logging only in the standalone variants, before app assembly:

fn run_production_selected<A, Initialize, Stop>(
    options: AxumRunOptions,
    signals: bool,
    stop: Stop,
    initialize: Initialize,
) -> anyhow::Result<()>
where /* unchanged */
{
    if signals {
        init_logging::<A>(options.logging_level())?;
    }
    run_local(/* … */, async move {
        let metadata = A::stores();
        options.validate_stores(metadata)?;
        let mut app = App::build::<A>(crate::AXUM_PLATFORM)
            .map_err(|_error| failure("configure", "application", "assembly failed"))?;
        // …
    })
}

Then document that embedders own their logger, and that with_logging_level applies only to the standalone entrypoints. Alternatively, treat SetLoggerError as success when embedding, without calling set_max_level first.

fn adopt_listener(source: ListenerSource) -> anyhow::Result<TokioTcpListener> {
let listener = match source {
ListenerSource::Bind(addr) => StdTcpListener::bind(addr)
.map_err(|_error| failure("bind", "listener", "address unavailable"))?,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 Dev startup lost the listening URL and mislabels bind failures

Before this PR, run_app logged starting axum server on http://{addr} and failed with failed to bind dev server to {addr}: {io error}. Now it logs native server ready (line 654) without an address. Every bind error becomes address unavailable, including permission denied on a port below 1024 and an IP that isn't local. demo_logging.rs was changed to expect the new text. This affects everyone using edgezero serve or run_app, and a parsed SocketAddr is not secret.

Fix:

ListenerSource::Bind(addr) => StdTcpListener::bind(addr).map_err(|error| {
    let category = match error.kind() {
        ErrorKind::AddrInUse => "address in use",
        ErrorKind::PermissionDenied => "permission denied",
        ErrorKind::AddrNotAvailable => "address not available",
        _ => "bind failed",
    };
    failure("bind", "listener", category)
})?,

and at line 654:

let bound = listener
    .local_addr()
    .map_err(|_error| failure("bind", "listener", "local address unavailable"))?;
log::info!("[edgezero] native server ready on http://{bound}");

}
#[cfg(unix)]
{
let interrupt = signal::unix::signal(signal::unix::SignalKind::interrupt())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 Existing AxumDevServer paths now take over SIGTERM

enable_ctrl_c (default true) now also registers a SIGTERM handler. That covers AxumDevServer::run() and the async embedding API serve() (through serve_local → run_local). tokio never unregisters signal handlers, so once a host has called serve(), SIGTERM no longer terminates that process. Before this PR only SIGINT was handled. run() also drains with a hard-coded Duration::from_secs(10) (line 180) that ignores SHUTDOWN_GRACE_SECONDS. The same literal appears at run_options.rs:141 and :302.

Suggest keeping the legacy enable_ctrl_c paths SIGINT-only (or adding a separate field for SIGTERM), plus a single DEFAULT_SHUTDOWN_GRACE constant. If the SIGTERM change is intended, document it on the field and in the Axum guide.

if let Some(phase) = self.phase.clone() {
core_request
.extensions_mut()
.insert(LifecycleReader::new(move || Some(*phase.borrow())));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ A LifecycleReader is allocated on every request

LifecycleReader::new allocates a new Arc-wrapped closure and clones the watch receiver on every request, including requests that never read it. Build the reader once in with_phase and store it next to the private receiver, which the gate keeps using. Insert a clone here; that's only an Arc reference-count bump.

);
assert!(!log.contains("INITIALIZED"));
TcpStream::connect_timeout(&addr, Duration::from_millis(50))
.expect_err("failed startup never accepts connections");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 Test gaps

  • This connect check runs after server.wait() has seen the child exit, so nothing can be listening and the check can't fail. To show the server never accepts before failing, probe while the process is still starting, as stop_during_startup_never_binds_or_becomes_ready does.
  • The fixture's development and bare-dev modes are never used, so no subprocess test covers run_app's new behavior: SIGTERM handling, draining for up to the grace period, and a second signal forcing exit.
  • The dev-path reuse of one KV handle for ids that resolve to the same name (build_kv_registry, dev_server.rs:1161) has no test. A unit test asserting that two such ids share a handle would lock in that fix.
  • The production init_logging path is never exercised because the fixture owns logging. An embedding case with default logging would have caught the double-install failure.


Base: [PR #275](https://github.com/stackpop/edgezero/pull/275), inspected at `5527b58e2a7cb6307d5c7a8926cd5dfc6f4f8b4a`.

Worktree: `/home/pav/worktrees/issue-392-396-production-hosting-spec`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏ Stale and local-only details in the design doc

  • This line commits a personal local path that means nothing to other readers.
  • Line 427 says the base "has since advanced to dfd439c…" and that integration is still open, while the PR body says the branch is ported to current feat: implement portable outbound HTTP and request lifecycle contracts #275. The Status line (5) and the "Review boundary" section should match the head under review.
  • The issue links point at stackpop/edgezero; the repo is now IABTechLab/edgezero.


/// Observed native runner phase. Once draining starts, a run never becomes ready again.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LifecyclePhase {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌱 Mark LifecyclePhase #[non_exhaustive] while it's new

It's a public enum in the portable crate, and apps that write their own probes will match on it. Adding a phase later (a degraded state, for example) would then be a breaking change. Marking it #[non_exhaustive] now costs nothing.

.config_dir()
.ok_or_else(|| failure("settings", "CONFIG_DIR", "required"))?;
let mut by_id = BTreeMap::new();
let limits = ConfigStoreLimits::from_env(options.env_config())?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏ Config-store limit validation depends on whether a config store is declared

from_os_vars rejects a non-Unicode EDGEZERO__ADAPTER__CONFIG_STORE__* value for every app (run_options.rs:278). A Unicode value such as abc is parsed only here, inside if let Some(meta) = metadata.config. So a production app with no config stores rejects the first and silently accepts the second. Either parse ConfigStoreLimits::from_env unconditionally next to AxumIngressConfig::from_env in run_production_selected, or drop the CONFIG_STORE arm from the non-Unicode check.

@dhruv8sh dhruv8sh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review

Summary

Production hosting for the Axum adapter: bounded draining, startup-time store/config extraction, and redacted startup errors. The design is solid and well tested; two behavioural questions need answers before merge.

😃 Praise

  • Moving extraction off RequestContext (AppConfig::from_binding, router.rs insert_state) lets startup and requests share the same limits, validation and redaction. Copy-on-write startup state with old-admission invalidation is tidy and covered by startup_state_forks_router_and_invalidates_old_admission.
  • Errors stay redacted end to end (single message, no source chain, sentinel tests). KV stores sharing a name now get one handle, which also fixes a redb lock conflict on the dev path. Draining is bounded and a second signal forces a non-success exit.

Findings

Blocking

  • ❓ Second embedded run in one process fails (crates/edgezero-adapter-axum/src/dev_server.rs, run_production_selected → init_logging::<A>): the global logger is installed per run, so a second run_app_with_options* call (restart, tests) fails with startup logging: application: initialization failed. Also, an App::build failure returns a bare "application configuration failed" without the startup …: prefix. Fix: make logger init idempotent (ignore already-set) or move it out of the embedding API; use failure("configure", "application", …) for the build error.
  • ❓ Ctrl-C behaviour change on dev server / run_app (dev_server.rs, AxumDevServer::run with Duration::from_secs(10), and run_app_with_preflight): previously Ctrl-C exited immediately with success; now it drains up to 10s, exits non-zero with "grace exhausted" if a streaming request is open, and also listens for SIGTERM. Not called out for serve/demo. Is this intended? If so document it; otherwise keep cancel semantics (Duration::ZERO) for dev entrypoints.

Non-blocking

  • 🤔 Possible busy loop if the phase sender is dropped (crates/edgezero-adapter-axum/src/connection.rs, draining future): if reader.changed() returns Err while phase is Ready, the loop spins. Unreachable today since run_local holds the sender, but fragile. Fix: on Err, treat as draining.
  • 🤔 Dev KV path lost diagnostics (key_value_store.rs, dev_server.rs build_kv_registry Optional branch): redaction also applies to dev, so local users no longer see the DB path or underlying error. Fix: redact only in production, or log the source at debug!.
  • 🤔 Test fixture ships as a binary in the published crate (crates/edgezero-adapter-axum/Cargo.toml, [[bin]] edgezero-hosting-fixture): gated by required-features but still packaged/installable. Fix: exclude, or move to a test-only crate.
  • 🤔 Port reservation race in subprocess tests (crates/edgezero-adapter-axum/tests/production_hosting.rs, address()): binds port 0, drops it, child rebinds — flaky under parallel tests. Fix: retry on bind failure, or have the child report its bound port.
  • 🤔 Two probe states are unobservable on the native runner (crates/edgezero-core/src/probe.rs): liveness returns 200 in Starting/Draining, but the listener isn't bound during Starting and dispatch refuses work while Draining. The spec acknowledges it; add one rustdoc line so the table doesn't mislead.
  • ⛏ Config-store NAME override validated but unused in production (run_options.rs validate_stores, dev_server.rs build_production_stores): files are always local-config-{id}.json. Document or skip validation.
  • ⛏ File-level #![expect(clippy::arbitrary_source_item_ordering)] in dev_server.rs — scope it to the new items.
  • ⛏ Leftover string indentation in crates/edgezero-core/src/extractor.rs resolve_leaf (UnknownStore message, [stores.secrets] (id redacted) line). Output unaffected; source only.

📌 Out of Scope

  • Removing pull_request.branches: [main] from .github/workflows/{codeql,format,test}.yml is a repo-wide CI policy change (needed for stacked PRs, unrelated to hosting). Consider splitting it out; ci_tooling.test.mjs only covers test.yml.

CI Status

  • fmt: PASS
  • clippy: PASS
  • tests: PASS (2727 passed, 0 failed)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Axum: make production configuration and local storage explicit Axum: define a production server lifecycle with bounded graceful shutdown

3 participants